// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;

namespace SnapObjects.Expressions
{
    internal static class DynamicCall
    {
        private static readonly IDictionary<string, List<MethodInfo>> _methods;

        static DynamicCall()
        {
            _methods = new ConcurrentDictionary<string, List<MethodInfo>>();

            //String
            LoadTypeReflections(typeof(string));

            //Regex
            LoadTypeReflections(typeof(Regex));

            //Queryable
            LoadTypeReflections(typeof(Queryable));
        }

        private static void LoadTypeReflections(Type type)
        {
            var methods = type.GetMethods();

            foreach (var method in methods)
            {
                if (method.Name == "ToString" && method.DeclaringType != typeof(object))
                {
                    continue;
                }

                if (!_methods.ContainsKey(method.Name))
                {
                    _methods.Add(method.Name, new List<MethodInfo>() { method });
                }
                else
                {
                    _methods[method.Name].Add(method);
                }
            }
        }

        private static bool TryGetMethod(string name, Expression[] parameters, out MethodInfo method, Type instanceType = null)
        {
            method = null;

            if (_methods.TryGetValue(name, out var infolist))
            {
                foreach (var info in infolist)
                {
                    var flag = true;

                    var args = info.GetParameters();

                    if (args.Length == parameters.Length)
                    {
                        if (args.Length == 0 && info.DeclaringType.IsAssignableFrom(instanceType))
                        {
                            method = info;
                        }
                        else if (args.Length == 0)
                        {
                            flag = false;
                        }

                        if (info.ContainsGenericParameters)
                        {
                            var len = info.GetGenericArguments().Length;

                            var genericTypes = new List<Type>();
                            for (var i = 0; i < len; i++)
                            {
                                var param = parameters[i];

                                if (param is LambdaExpression lexp)
                                {
                                    genericTypes.Add(lexp.ReturnType);
                                }
                                else
                                {
                                    var argumentTypes = param.Type.GetGenericArguments();

                                    if (argumentTypes.Length > 0)
                                    {
                                        genericTypes.Add(argumentTypes[0]);
                                    }
                                    else
                                    {
                                        genericTypes.Add(param.Type);
                                    }
                                }
                            }

                            method = info.MakeGenericMethod(genericTypes.ToArray());

                            args = method.GetParameters();
                        }

                        for (var i = 0; i < args.Length; i++)
                        {
                            var argType = args[i].ParameterType;

                            if (args[i].ParameterType.BaseType == typeof(LambdaExpression))
                            {
                                argType = args[i].ParameterType.GetGenericArguments()[0];
                            }

                            if (argType != parameters[i].Type)
                            {
                                var type = parameters[i].Type.GetInterface(args[i].ParameterType.Name, true);
                                if (null == type)
                                {
                                    flag = false;
                                    break;
                                }
                            }
                            else if (null == method)
                            {
                                method = info;
                            }
                        }

                        if (!flag)
                        {
                            method = null;
                            continue;
                        }
                        else
                        {
                            break;
                        }
                    }
                }

                return method != null;
            }

            return false;
        }

        public static Expression GetExpression(string name, Expression instance, params Expression[] parameters)
        {
            if (TryGetMethod(name, parameters, out var method, instance?.Type))
            {
                if (method.IsGenericMethodDefinition)
                {
                    var types = new List<Type>();

                    foreach (var item in parameters)
                    {
                        types.Add(item.Type);
                    }

                    method = method.MakeGenericMethod(types.ToArray());
                }

                if (method.IsStatic)
                {
                    return Expression.Call(null, method, parameters);
                }
                else
                {
                    return Expression.Call(instance, method, parameters);
                }
            }
            return instance;
        }

        public static Expression IsMatch(Expression value, string pattern)
        {
            Expression expression = Expression.Constant(new Regex(pattern));

            if (TryGetMethod(nameof(Regex.IsMatch), new Expression[] { value }, out var method))
            {
                return Expression.Call(expression, method, value);
            }

            return value;
        }

        public static Expression NullableValue(Expression instance)
        {
            var property = instance.Type.GetProperty("Value");

            return Expression.MakeMemberAccess(instance, property);
        }

        public static Expression GetNullableExpression(Expression expression)
        {
            var type = typeof(Nullable<>).MakeGenericType(expression.Type);

            var constructor = type.GetConstructor(new Type[] { expression.Type });

            var expr = Expression.New(constructor, expression);

            if (expression.NodeType == ExpressionType.Constant)
            {
                return Expression.Constant(expr);
            }

            return expr;
        }
    }
}
